# -*- coding:utf-8 -*-

import os
from typing import NoReturn

import cv2
import numpy as np

from src._typing import MatchLocation
from src._errors import FileTypeError, FileNameError
from src._base import logger

__all__ = ('is_img', 'TemplateMatch')


class TemplateMatch:

    # openCV 读取的原始图片
    _cv_src = None
    # openCV 读取的模板图片
    _cv_template = None

    def __init__(self, src_image: str | None = None):
        self.src_img = src_image
        self._template = None

    @staticmethod
    def _check_path(file_path: str) -> bool:
        """
        检查文件全路径是否为纯英文
        文件包含中文名时异常，OpenCV不支持中文文件名
        Args:
            file_path(str): 文件完整路径，包含文件名

        Returns:
            NoReturn

        Raises:
            FileNameError: 文件全路径非法，不支持含中文字符的文件名!

        """
        from src._items.other_handle import StringCheck
        check = StringCheck()
        if check.is_chinese(file_path):
            raise FileNameError('文件全路径非法，不支持含中文字符的文件名!')
        else:
            logger.info(f'文件全路径：{file_path}')
            return True

    @property
    def src_img(self) -> str:
        return self._src_img

    @property
    def template(self) -> str:
        return self._template

    @src_img.setter
    def src_img(self, s_path_: str) -> NoReturn:
        """
        设置源图片路径，同时判断文件是否存在，不存在，则抛异常
        Args:
            s_path_(str): 源图片文件路径

        Returns:
            NoReturn

        """
        # self._check_path(s_path_)
        if os.path.exists(s_path_):
            self._src_img = s_path_
        else:
            raise FileNotFoundError(f'文件路径不存在：{s_path_}')

    @template.setter
    def template(self, t_path_: str) -> NoReturn:
        """
        设置模板图片路径，同时判断文件是否存在，不存在，则抛异常
        Args:
            t_path_(str): 模板图片文件路径

        Returns:
            NoReturn

        """
        # self._check_path(t_path_)
        if os.path.exists(t_path_):
            self._template = t_path_
        else:
            raise FileNotFoundError(f'文件路径不存在：{t_path_}')

    def __init_src_image(self):
        self._cv_src = cv2.imread(self._src_img)

    def __init_template_image(self):
        self._cv_template = cv2.imread(self._template)

    def get_template_location(self, template: str,
                              matching_algorithm: int = cv2.TM_CCOEFF_NORMED) -> MatchLocation | None:
        """
        获取模板匹配到的位置信息
        Args:
            template(str): 模板文件路径,目标匹配的模板文件路径
            matching_algorithm(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:
            match_location(MatchLocation | None): 匹配到的位置信息，None为未匹配到

        """
        match_location = self._cv_match(template, cv_function=matching_algorithm)
        if match_location is None:
            logger.error('未找到匹配项!')
        # else:
        #     logger.info(f'匹配项信息：{match_location}')
        return match_location

    def get_template_locations(self, template: str,
                               matching_algorithm: int = cv2.TM_CCOEFF_NORMED) -> list[MatchLocation] | list:
        """
        获取模板匹配到的位置信息，单模板，多目标匹配
        Args:
            template(str): 模板文件路径,目标匹配的模板文件路径
            matching_algorithm(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:

        """
        match_location = self._cv_match_all(template, cv_function=matching_algorithm)
        if not match_location:
            logger.error('未找到匹配项!')
        else:
            logger.info(f'匹配项信息：{match_location}')
        return match_location

    def get_templates_locations(self, templates: list[str],
                                matching_algorithm: int = cv2.TM_CCOEFF_NORMED) -> list[MatchLocation] | list:
        """
        获取模板匹配到的位置信息，多模板，单目标匹配
        Args:
            templates(list[str]): 模板文件路径列表,目标匹配的模板文件路径列表
            matching_algorithm(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:

        """
        match_location = []
        for tmpl in templates:
            match_loc = self._cv_match(tmpl, cv_function=matching_algorithm)
            if not match_loc:
                logger.error(f'未找到匹配项!模板文件--{tmpl}')
            else:
                match_location.append(match_loc)
                logger.info(f'文件（{tmpl}）匹配项信息：{match_loc}')
        return match_location

    def is_match(self, template: str, matching_algorithm: int = cv2.TM_CCOEFF_NORMED) -> bool:
        """
        判断模板是否匹配
        Args:
            template(str): 模板文件路径
            matching_algorithm(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:
            bool: 是否匹配成功
        """
        match_location = self.get_template_location(template, matching_algorithm=matching_algorithm)
        return match_location is not None

    def is_all_match(self, templates: list[str] | str, match_count: int = 0,
                     matching_algorithm: int = cv2.TM_CCOEFF_NORMED) -> bool:
        """
        判断所有模板是否匹配
        Args:
            templates(list[str]): 模板文件路径列表
            match_count(int): 匹配数量，单模板多目标匹配时，用于辅助校验是否全部匹配成功。默认为0
            matching_algorithm(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:
            bool: 是否全部匹配成功--匹配到的数量是否符合预期

        """
        if isinstance(templates, str):
            match_locations = self.get_template_locations(templates, matching_algorithm=matching_algorithm)
            if match_count != 0:
                res_count = match_count
            else:
                res_count = len(match_locations)
                logger.warning(f'未指定匹配数量，默认使用当前匹配到的数量：{res_count}')
        else:
            match_locations = self.get_templates_locations(templates, matching_algorithm=matching_algorithm)
            res_count = len(templates)
        return len(match_locations) == res_count

    def __base_check(self, template: str):
        """
        基础检查
        包括--文件是否存在，文件路径是否包含中文，文件是否是图像文件
        Args:
            template(str): 模板文件路径

        Returns:
            NoReturn

        Raises:
            FileTypeError: 文件类型异常，不是图像文件!

        """
        # 判断文件是否存在
        # self.src_img = img
        self.template = template
        # 判断文件是否包含中文
        self._check_path(self._src_img)
        self._check_path(template)
        # 判断文件是否是图像文件
        if is_img(self._src_img) and is_img(template):
            # 是，则初始化
            self.__init_src_image()
            self.__init_template_image()
        else:
            raise FileTypeError('文件类型异常，不是图像文件!')

    def _cv_match(self, template: str, cv_function: int = cv2.TM_CCOEFF_NORMED) -> tuple[float, float] | None:
        """
        使用OpenCV库，进行模板匹配
        Args:
            template(str): 模板文件路径
            cv_function(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:
            res_match(list[float, float]|None): 匹配结果，处理为匹配到的矩形图像的中心点偏移位置坐标
        """
        res_match = None
        if cv_function not in (cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED):
            logger.warning(
                f'当前算法（{cv_function}）不在支持的算法中，将使用cv2.TM_CCOEFF_NORMED（{cv2.TM_CCOEFF_NORMED}）算法')
            cv_function = cv2.TM_CCOEFF_NORMED
        try:
            self.__base_check(template)
            # 获取模板图像的宽度和高度
            t_h, t_w, c = self._cv_template.shape
            # 进行模板匹配
            # 增加指定算法处理，方便动态选择不同的算法，查看效果
            match_res = cv2.matchTemplate(self._cv_src, self._cv_template, cv_function)
            # print(f'match_res: {match_res}')
            min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(match_res)
            # print(f'min_val: {min_val}, max_val: {max_val}, min_loc: {min_loc}, max_loc: {max_loc}')
            match cv_function:
                case cv2.TM_SQDIFF_NORMED:
                    nearest_point = min_loc
                    tar_match_val = min_val
                    tar_match_bool = min_val < 0.1
                case _:
                    nearest_point = max_loc
                    tar_match_val = max_val
                    tar_match_bool = max_val > 0.8
            logger.info(f'模板匹配值: {tar_match_val}')
            # 匹配到合理的结果再进行坐标处理
            if tar_match_bool:
                result_point = (float(nearest_point[0] + t_w / 2), float(nearest_point[1] + t_h / 2))
                # print(f'nearest_point: {nearest_point}， result_point: {result_point}')
                res_match = result_point
            else:
                logger.warning('匹配结果不在置信区间内，将舍弃!')
            # # 在原图像上绘制矩形，显示匹配结果
            # r_point = (nearest_point[0] + t_w, nearest_point[1] + t_h)
            # cv2.rectangle(self._cv_src, nearest_point, r_point, (0, 0, 255), 2)
            # cv2.imshow('Result', self._cv_src)
            # cv2.waitKey(0)
        except Exception as err:
            logger.error(f'异常信息: {err.__repr__()}', exc_info=err.__traceback__)
        return res_match

    @staticmethod
    def __choice_location(loc_a: tuple, loc_b: tuple,
                          template_w: int | float, template_h: int | float) -> list[tuple]:
        """
        计算筛选坐标
        Args:
            loc_a(tuple): 坐标信息
            loc_b(tuple): 坐标信息
            template_w(int | float): 模板宽度
            template_h(int | float): 模板高度

        Returns:
            res_list(list[tuple]): 筛选后的坐标列表

        """
        x_ = abs(loc_a[0] - loc_b[0])
        y_ = abs(loc_a[1] - loc_b[1])
        t_w = template_w / 2
        t_h = template_h / 2
        if x_ <= t_w and y_ <= t_h:
            if loc_a[0] < loc_b[0] or loc_a[1] < loc_b[1]:
                return [loc_a]
            else:
                return [loc_b]
        else:
            return [loc_a, loc_b]

    def __filter_location_list(self, loc_list: list[tuple],
                               template_w: int | float,
                               template_h: int | float) -> list[tuple]:
        """
        筛选坐标列表
        Args:
            loc_list(list[tuple]): 坐标列表
            template_w(int | float): 模板宽度
            template_h(int | float): 模板高度

        Returns:
            res_list(list[tuple]): 筛选后的坐标列表

        """
        import random
        # 剩余坐标信息列表
        remain_list = loc_list
        # 最终结果列表
        result_list = []
        while remain_list:
            # 取随机2个坐标，剩余坐标数量小于2 时，取1
            random_length = min(len(remain_list), 2)
            tmp_list = random.sample(remain_list, random_length)
            if len(remain_list) >= 2:
                # tmp_list = [remain_list[0], remain_list[1]]
                # 筛选过滤坐标
                ch_list = self.__choice_location(tmp_list[0], tmp_list[1], template_w, template_h)
            else:
                # tmp_list = remain_list
                # 剩1 个坐标时，随机取一个已筛选过的坐标值进行过滤
                ch_list = self.__choice_location(tmp_list[0], random.choice(result_list), template_w, template_h)
            result_list.extend(ch_list)
            # 更新剩余坐标信息列表
            remain_list = list(set(remain_list) - set(tmp_list))

        # 去重
        result_list = list(set(result_list))
        return result_list

    def _cv_match_all(self, template: str, cv_function: int = cv2.TM_CCOEFF_NORMED) -> list[tuple] | list:
        """
        使用OpenCV库，进行模板匹配
        Args:
            template(str): 模板文件路径
            cv_function(int): 模板匹配算法，默认cv2.TM_CCOEFF_NORMED
                目前仅支持归一化的算法，如cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED

        Returns:
            res_match(list): 匹配结果，处理为匹配到的矩形图像的中心点偏移位置坐标
        """
        res_match = []
        if cv_function not in (cv2.TM_CCOEFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_SQDIFF_NORMED):
            logger.warning(
                f'当前算法（{cv_function}）不在支持的算法中，将使用cv2.TM_CCOEFF_NORMED（{cv2.TM_CCOEFF_NORMED}）算法')
            cv_function = cv2.TM_CCOEFF_NORMED
        try:
            # 基础检查，并，openCV初始化两个图片
            self.__base_check(template)
            # 获取模板图像的宽度和高度
            t_h, t_w, c = self._cv_template.shape
            # 进行模板匹配
            match_res = cv2.matchTemplate(self._cv_src, self._cv_template, cv_function)
            # 设定阈值
            min_threshold = 0.1
            max_threshold = 0.8
            match cv_function:
                case cv2.TM_SQDIFF_NORMED:
                    loc = np.where(match_res <= min_threshold)
                case _:
                    # 获取匹配结果超过阈值的位置
                    loc = np.where(match_res >= max_threshold)
            # 根据位置绘制矩形框
            for pt in zip(*loc[::-1]):
                result_point = (float(pt[0] + t_w / 2), float(pt[1] + t_h / 2))
                res_match.append(result_point)
            # 能搜到结果再进行过滤
            if res_match:
                # 对多目标进行筛选、过滤
                filter_list = res_match
                # 调用过滤算法次数为当前长度的一半加2
                # 经过测试 +1 时，偶尔会出现少1次过滤结果的情况，故，再加1 次
                filter_times = len(res_match) // 2 + 2
                for i in range(filter_times):
                    filter_list = self.__filter_location_list(filter_list, t_w, t_h)
                res_match = filter_list
            # print(f'filter_list: {filter_list}')
            #     # 画图
            #     cv2.rectangle(o_src, pt, (pt[0] + t_w, pt[1] + t_h), (0, 0, 255), 2)
            #
            # # 显示图像
            # cv2.imshow('Matched Areas', o_src)
            # cv2.waitKey(0)
            # cv2.destroyAllWindows()

            # # 创建图像金字塔
            # pyramid = [cv2.pyrDown(o_src) for _ in range(4)]
            #
            # found = None
            #
            # for img_p in pyramid:
            #     # 在当前层图像中寻找模板
            #     res = cv2.matchTemplate(img_p, o_template, cv_function)
            #     loc = np.where(res >= 0.8)
            #
            #     if loc[0].shape[0] > 0 and loc[1].shape[0] > 0:
            #         found = (loc, res[loc[0][0], loc[1][0]])
            #     else:
            #         found = None
            #
            # if found is not None:
            #     loc, score = found
            #     print(f'Found a match with score: {score}')
            #     cv2.rectangle(img, (loc[1][0], loc[0][0]), (loc[1][0] + t_w, loc[0][0] + t_h), (0, 0, 255), 2)

        except Exception as err:
            logger.error(f'异常信息: {err.__repr__()}', exc_info=err.__traceback__)
        return res_match


def is_img(img) -> bool:
    from PIL import Image
    try:
        # with  as f:
        Image.open(img)
        return True
    except Exception as err:
        logger.error(f'异常信息: {err.__repr__()}')
        return False


if __name__ == '__main__':
    s_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test_pictures/four_people.png'))
    t_match = TemplateMatch(s_path)
    import os
    # ma_jiang.jpeg
    # four_people.png
    # desk_icon

    # pdf_templ.jpeg
    # light.png
    t_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test_pictures/light.png'))
    # r_s_path = r'E:/WORK/Python/canvas_auto/four_people.png'
    # test_s = cv2.imread(s_path)
    # print(f'shape: {test_s.shape}')
    match_result = t_match._cv_match(t_path)
    print(f'match_result: {match_result}')
    # match_list = t_match.cv_match_all(s_path, t_path)
    # print(f'match_list: {match_list}')
